/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.gwt.libideas.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * A class to choose the best locale given a list of supported locales and
 * an HTTP Accept-Language header.
 */
public class LocaleMatcher {

  /**
   * Class representing entries in an Accept-Language header.
   */
  private static class AcceptLanguageEntry implements Comparable<AcceptLanguageEntry> {
    
    public String language;
    public double priority;
    
    public AcceptLanguageEntry(String language, double priority) {
      this.language = language;
      this.priority = priority;
    }

    /**
     * Compare in descending order, since a priority of 1.0 is higher than a 0.8
     */
    public int compareTo(AcceptLanguageEntry other) {
      if (priority < other.priority) {
        return 1;
      } else if (priority > other.priority) {
        return -1;
      } else {
        return 0;
      }
    }

    @Override
    public String toString() {
      return language + " (" + priority + ")";
    }
  }
  
  /**
   * Set of supported locales.  The value is the unmangled locale code, but it
   * is stored in the map smashed to lowercase for ease of matching.
   */
  private Map<String, String> supportedLocales = new HashMap<String, String>();
  
  /**
   * Construct a locale matcher given a collection of locales.
   * 
   * The collection of supported locales would commonly be generated by
   * {@link com.google.gwt.libideas.linker.LocaleListLinker} or some
   * other custom linker.
   * 
   * @param locales a collection of supported locales
   */
  public LocaleMatcher(Iterable<String> locales) {
    for (String locale : locales) {
      if (!"default".equals(locale)) {
        supportedLocales.put(locale.toLowerCase(), locale);
      }
    }
  }

  /**
   * Construct a locale matcher given a stream with a list of
   * locales, one per line.  The stream should be UTF8 (typically just ASCII).
   * 
   * The file containing the list of supported locales would commonly be
   * generated by {@link com.google.gwt.libideas.linker.LocaleListLinker}.
   * 
   * @param localeListStream input stream to read locale list from -- if null,
   *     no locales are read.
   * @throws IOException if an error occurs reading the stream
   */
  public LocaleMatcher(InputStream localeListStream) throws IOException {
    if (localeListStream == null) {
      return;
    }
    BufferedReader reader = new BufferedReader(new InputStreamReader(localeListStream, "UTF-8"));
    String line;
    while ((line = reader.readLine()) != null) {
      if (!"default".equals(line)) {
        supportedLocales.put(line.toLowerCase(), line);
      }
    }
  }
  
  /**
   * Select the best match from the list of available locales.  If no match
   * is found, "default" will be returned.
   * 
   * The Accept-Languages header is of the format:
   *   locale[;q=number][,locale[;q=number]]*
   * 
   * The numbers are priorities between 0 and 1, with a higher value indicating
   * higher preference.  Note that Accept-Language locale identifiers only include
   * a language and a country code, and they are separated by dashes not underscores.
   * 
   * @param acceptLang the Accept-Languages header
   * @return the selected locale name
   */
  public String findBestMatch(String acceptLang) {
    if (acceptLang != null) {
      // Break into individual language entries.
      ArrayList<AcceptLanguageEntry> languages = new ArrayList<AcceptLanguageEntry>();
      int pos = 0;
      while (pos < acceptLang.length()) {
        int comma = acceptLang.indexOf(',', pos);
        String oneLang;
        if (comma >= 0) {
          oneLang = acceptLang.substring(pos, comma);
          pos = comma + 1;
        } else {
          oneLang = acceptLang.substring(pos);
          pos = acceptLang.length();
        }
        double priority = 1.0;
        int semi = oneLang.indexOf(";q=");
        if (semi >= 0) {
          priority = Double.valueOf(oneLang.substring(semi + 3));
          oneLang = oneLang.substring(0, semi);
        }
        if (priority > 0) {
          languages.add(new AcceptLanguageEntry(oneLang, priority));
        }
      }
      // Look for exact matches first.
      Collections.sort(languages);
      for (AcceptLanguageEntry language : languages) {
        String languageName = language.language;
        languageName = languageName.replace('-', '_').toLowerCase();
        String match = supportedLocales.get(languageName);
        if (match != null) {
          return match;
        }
      }
      // No exact match, try stripping any country tags from each entry.
      for (AcceptLanguageEntry language : languages) {
        String languageName = language.language;
        pos = languageName.indexOf('-');
        if (pos >= 0) {
          languageName = languageName.substring(0, pos).toLowerCase();
          String match = supportedLocales.get(languageName);
          if (match != null) {
            return match;
          }
        }
      }
    }
    return "default";
  }
}
